package com.fowo.api.common.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fowo.api.common.entity.IdObject;
import com.fowo.api.common.model.BatchActionModal;
import com.fowo.api.common.model.R;
import com.fowo.api.common.model.RException;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.core.MethodParameter;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;

/**
 * 提供支持各种基本操作的通用控制器基类
 */
public abstract class CommonController {

    @Resource
    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;

    /**
     * 批处理
     */
    @Operation(summary = "批处理操作")
    @PostMapping("batch")
    @Transactional(rollbackFor = Exception.class)
    public R batch(@RequestBody BatchActionModal batchActionModal) throws Exception {
        String method = batchActionModal.getMethod();
        if (StringUtils.hasText(method)) {
            method = method.toUpperCase();
        } else {
            method = "GET";
        }
        String path = batchActionModal.getPath();
        Method controllerMethod = findBatchControllerMethod(method, path);
        if (controllerMethod != null) {
            return runBatch(controllerMethod, batchActionModal);
        }
        return R.fail("此批量操作不被支持！");
    }

    protected R runBatch(Method controllerMethod, BatchActionModal batchActionModal) throws Exception {
        if(controllerMethod.trySetAccessible()) {
            for (Object arg : batchActionModal.getArgs()) {
                runBatchWithArg(controllerMethod, arg);
            }
            return R.success("批量操作已完成", null);
        }
        return R.fail("无法访问批量方法");
    }

    protected void runBatchWithArg(Method controllerMethod, Object arg) throws Exception {
        Object[] args = solveRunBatchMethodArgs(controllerMethod, arg);
        Object result = controllerMethod.invoke(this, args);
        if (result instanceof R) {
            if(!((R<?>) result).isSuccess()) {
                throw new RException((R) result);
            }
        }
    }

    protected Object[] solveRunBatchMethodArgs(Method controllerMethod, Object arg) throws Exception {
        if(arg == null) {
            return new Object[0];
        }
        Object[] parameterValues = new Object[controllerMethod.getParameterCount()];
        if(!(arg instanceof List)) {
            // 不是列表，直接表示单个参数
            Object oldArg = arg;
            arg = new ArrayList();
            ((List)arg).add(oldArg);
        }
        List<Object> args = (List<Object>) arg;
        ObjectMapper objectMapper = new ObjectMapper();
        final Parameter[] parameters = controllerMethod.getParameters();
        final List<HandlerMethodArgumentResolver> argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers();
        for(int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex ++) {
            Parameter parameter = parameters[parameterIndex];
            MethodParameter methodParameter = new MethodParameter(controllerMethod, parameterIndex);
            Object value = args.get(parameterIndex);
            if (value != null) {
                String valueJson = objectMapper.writeValueAsString(value);
                parameterValues[parameterIndex] = objectMapper.readValue(valueJson, parameter.getType());
            } else {
                // 调用自动方法参数解决处理
                final Optional<HandlerMethodArgumentResolver> first = argumentResolvers.stream().filter(r -> r.supportsParameter(methodParameter)).findFirst();
                if (first.isPresent()) {
                    parameterValues[parameterIndex] = first.get().resolveArgument(methodParameter, null, null, null);
                }
            }
        }

        return  parameterValues;
    }

    private final ConcurrentMap<String, Method> controllerMethodMaps = new ConcurrentHashMap<>();

    private Method findBatchControllerMethod(String method, String path) throws RException {
        String actionKey = String.format("%s:%s", method, path);
        Method controllerMethod = controllerMethodMaps.get(actionKey);
        if (controllerMethod == null) {
            final Method[] methods = this.getClass().getMethods();
            final List<Method> matchMethods = Arrays.stream(methods).filter(m -> isMethodMatch(m, method, path)).collect(Collectors.toList());
            if (matchMethods.isEmpty()) {
                throw new RException(String.format("批量操作模式 %s 未能找到控制器方法", actionKey));
            }
            if (matchMethods.size() > 1) {
                throw new RException(String.format("批量操作模式 %s 找到多个控制器方法", actionKey));
            }
            controllerMethod = matchMethods.get(0);
            controllerMethodMaps.put(actionKey, controllerMethod);
        }
        return controllerMethod;
    }

    protected boolean isMethodMatch(Method m, String method, String path) {
        final RequestMapping requestMapping = m.getAnnotation(RequestMapping.class);
        if("GET".equals(method)) {
            if(isMethodMatch(m, requestMapping, RequestMethod.GET, path)) {
                return true;
            }
        }
        if("POST".equals(method)) {
            if(isMethodMatch(m, requestMapping, RequestMethod.POST, path)) {
                return true;
            }
        }
        if("PUT".equals(method)) {
            if(isMethodMatch(m, requestMapping, RequestMethod.PUT, path)) {
                return true;
            }
        }
        if("DELETE".equals(method)) {
            if(isMethodMatch(m, requestMapping, RequestMethod.DELETE, path)) {
                return true;
            }
        }
        if("PATCH".equals(method)) {
            if(isMethodMatch(m, requestMapping, RequestMethod.PATCH, path)) {
                return true;
            }
        }
        return false;
    }

    private boolean isMethodMatch(Method m, RequestMapping requestMapping, RequestMethod requestMethod, String path) {
        if(requestMapping != null && Arrays.stream(requestMapping.method()).anyMatch(rm -> rm == requestMethod) && isMethodPathMatch(requestMapping.path(), path)) {
            return true;
        }
        if(requestMethod == RequestMethod.GET) {
            final GetMapping getMapping = m.getAnnotation(GetMapping.class);
            if (getMapping != null && isMethodPathMatch(getMapping.value(), path)) {
                return true;
            }
        }
        if(requestMethod == RequestMethod.POST) {
            final PostMapping postMapping = m.getAnnotation(PostMapping.class);
            if (postMapping != null && isMethodPathMatch(postMapping.value(), path)) {
                return true;
            }
        }
        if(requestMethod == RequestMethod.PUT) {
            final PutMapping putMapping = m.getAnnotation(PutMapping.class);
            if (putMapping != null && isMethodPathMatch(putMapping.value(), path)) {
                return true;
            }
        }
        if(requestMethod == RequestMethod.DELETE) {
            final DeleteMapping deleteMapping = m.getAnnotation(DeleteMapping.class);
            if (deleteMapping != null && isMethodPathMatch(deleteMapping.value(), path)) {
                return true;
            }
        }
        if(requestMethod == RequestMethod.PATCH) {
            final PatchMapping patchMapping = m.getAnnotation(PatchMapping.class);
            if (patchMapping != null && isMethodPathMatch(patchMapping.value(), path)) {
                return true;
            }
        }
        return false;
    }

    private boolean isMethodPathMatch(String[] path, String matchPath) {
        if(!StringUtils.hasText(matchPath) && path.length == 0) {
            return true;
        }
        return Arrays.stream(path).anyMatch(p -> p.equals(matchPath));
    }
}
